gh-79012: Add asyncio chat server HOWTO#144604
Conversation
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
| Common pitfalls | ||
| =============== |
There was a problem hiding this comment.
This section isn't really about the chat server. These are just common asyncio traps that would fit better in the tutorial or the reference docs.
- Explain concepts (start_server, StreamReader/StreamWriter) before code - Use asyncio.TaskGroup for concurrent broadcasting - Use contextlib.suppress instead of bare except/pass - Remove Python test client, keep only nc/telnet - Properly explain asyncio.timeout before showing usage - Move implementation notes to code comments - Remove Exercises and Common pitfalls sections - Reorder seealso links in asyncio.rst Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
|
I have made the requested changes; please review again |
|
Thanks for making the requested changes! : please review the changes made to this pull request. |
- Move write/drain and close/wait_closed explanations above the echo server example - Explain async with server, serve_forever, and asyncio.run - Break chat server into subsections: client tracking, broadcasting, then the complete example - Show broadcast function separately before the full listing Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
|
I have made the requested changes; please review again |
|
Thanks for making the requested changes! : please review the changes made to this pull request. |
| one at a time. :func:`contextlib.suppress` silently handles any | ||
| :exc:`ConnectionError` from clients that have already disconnected:: |
There was a problem hiding this comment.
I don't think we need to explain contextlib.suppress -- that's not part of asyncio and should fall under the umbrella of "basic Python knowledge". If you want, we can add a comment in the example explaining what it does.
| Bob: Hi Alice! | ||
| Hello Bob! | ||
|
|
||
| Each message you type is broadcast to all other connected users. |
There was a problem hiding this comment.
| Each message you type is broadcast to all other connected users. | |
| Each message you type is broadcasted to all other connected users. |
| Adding an idle timeout | ||
| ====================== | ||
|
|
||
| To disconnect clients who have been idle for too long, wrap the read call in | ||
| :func:`asyncio.timeout`. This async context manager takes a duration in | ||
| seconds. If the enclosed ``await`` does not complete within that time, the | ||
| operation is cancelled and :exc:`TimeoutError` is raised. This frees server | ||
| resources when clients connect but stop sending data. | ||
|
|
||
| Replace the message loop in ``handle_client`` with:: |
There was a problem hiding this comment.
This section should go above the complete example, and then the timeout code should be in the full code.
| Before building the chat server, let's start with something simpler: an echo | ||
| server that sends back whatever a client sends. | ||
|
|
||
| The core of any asyncio network server is :func:`asyncio.start_server`. You | ||
| give it a callback function, a host, and a port. When a client connects, | ||
| asyncio calls your callback with two arguments: a | ||
| :class:`~asyncio.StreamReader` for receiving data and a | ||
| :class:`~asyncio.StreamWriter` for sending data back. Each connection runs | ||
| as its own coroutine, so multiple clients are handled concurrently. |
There was a problem hiding this comment.
It would be nice if we provided a small example of this before diving into the full echo server. We should start with something small, and then slowly build on that as we introduce concepts to the user.
| The :meth:`~asyncio.StreamWriter.write` method buffers data without sending | ||
| it immediately. Awaiting :meth:`~asyncio.StreamWriter.drain` flushes the | ||
| buffer and applies back-pressure if the client is slow to read. Similarly, | ||
| :meth:`~asyncio.StreamWriter.close` initiates shutdown, and awaiting | ||
| :meth:`~asyncio.StreamWriter.wait_closed` waits until the connection is | ||
| fully closed. | ||
|
|
||
| Using the server as an async context manager (``async with server``) ensures | ||
| it is properly cleaned up when done. Calling | ||
| :meth:`~asyncio.Server.serve_forever` keeps the server running until the | ||
| program is interrupted. Finally, :func:`asyncio.run` starts the event loop | ||
| and runs the top-level coroutine. |
There was a problem hiding this comment.
Same here -- let's introduce both these concepts with their own examples.
| asyncio.run(main()) | ||
|
|
||
| To test, run the server in one terminal and connect from another using ``nc`` | ||
| (or ``telnet``): |
There was a problem hiding this comment.
Why is this in parentheses?
| (or ``telnet``): | |
| or ``telnet``: |
| We store each connected client's name and :class:`~asyncio.StreamWriter` in a | ||
| module-level dictionary. When a client connects, ``handle_client`` prompts for | ||
| a name and adds the writer to the dictionary. A ``finally`` block ensures the | ||
| client is always removed on disconnect, even if the connection drops | ||
| unexpectedly. |
There was a problem hiding this comment.
This is introduced as a solution, but we didn't explain the problem. Why do we need to store writers? Why do we need to remove them?
|
FTR, you don't have to say "I have made the requested changes" unless someone actually requested changes. You can just re-request a review. |
| return | ||
|
|
||
| name = data.decode().strip() | ||
| connected_clients[name] = writer |
There was a problem hiding this comment.
You may mention that the code doesn't handle two clients with the same name properly, it's left as an exercice to the reader :-)
| name = data.decode().strip() | ||
| connected_clients[name] = writer | ||
| print(f'{name} ({addr}) has joined') | ||
| await broadcast(f'*** {name} has joined the chat ***\n', sender=name) |
There was a problem hiding this comment.
Maybe move this code in the try/finally block to cleanup the client if broadcast() raises ConnectionError?
- Introduce concepts incrementally with small examples before full code - Split echo server into subsections: accepting connections, running the server, reading and writing data - Explain the problem (why track clients?) before showing the solution - Move idle timeout section above the complete example and integrate timeout into the full chat server code - Move join broadcast inside try block for proper cleanup on error - Add note about duplicate client names (exercise for the reader) - Replace contextlib.suppress prose explanation with inline comment - Fix telnet parentheses, apply "broadcasted" wording Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
|
I have made the requested changes; please review again |
|
Thanks for making the requested changes! : please review the changes made to this pull request. |
|
Considering the author is now restricted, I'm closing their corresponding PRs. |
Summary
Adds a focused HOWTO for building a TCP chat server with asyncio streams, as suggested by @ZeroIntensity in the review of #144594:
The guide progressively builds from an echo server to a full chat server:
start_server,StreamReader/StreamWriter, write/drain, close/wait_closed)asyncio.timeout, plus exercisesNo overlap with the existing Conceptual Overview — this is purely a practical, hands-on HOWTO.
Test plan
make -C Doc checkpassesmake -C Doc htmlpasses (no warnings)🤖 Generated with Claude Code
📚 Documentation preview 📚: https://cpython-previews--144604.org.readthedocs.build/